{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Lab 2: 鲍鱼数据集回归\n",
    "\n",
    "## 数据描述\n",
    "\n",
    "在这个实验中，我们将使用深度学习来完成鲍鱼数据集回归任务，数据集 `Abalone_2000.csv` 包含了 2000 条数据，任务目标是通过各种物理测量来预测鲍鱼的年龄。以下是数据集中相关变量的描述：\n",
    "\n",
    "- Sex: 性别（M：雄性，F：雌性，I：幼体）\n",
    "- Length: 最长壳长度测量值（单位：毫米）\n",
    "- Diameter: 壳的直径，与长度垂直（单位：毫米）\n",
    "- Height: 带肉的壳的高度（单位：毫米）\n",
    "- Whole_weight: 整个鲍鱼的重量（单位：克）\n",
    "- Whole weight.1: 去壳后肉的重量（单位：克）\n",
    "- Whole weight.2: 内脏重量（去血后的肠道重量，单位：克）\n",
    "- Shell_weight: 壳干燥后的重量（单位：克）  \n",
    "\n",
    "- Rings: 环数，表示鲍鱼的年龄（单位：年）\n",
    "\n",
    "## 评估指标\n",
    "\n",
    "均方对数误差 (Root Mean Squared Logarithmic Error, RMSLE)，它用于衡量预测值$\\hat{y}_i$和真实值$y_i$之间的对数误差。公式如下：  \n",
    "\n",
    "$$\n",
    "RMSLE = \\sqrt{\\frac{1}{n} \\sum_{i=1}^{n} \\left( \\log(1 + \\hat{y}_i) - \\log(1 + y_i) \\right)^2 }\n",
    "$$  \n",
    "\n",
    "## 要求\n",
    "\n",
    "50个epoch内，在测试集上的RMSLE小于0.16\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据预处理\n",
    "\n",
    "我们先读取并观察数据的格式\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "df = pd.read_csv(\"Abalone_2000.csv\")\n",
    "df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，我们的数据集中有 2000 个样本，每个样本有 8 个特征和 1 个标签（Rings）。\n",
    "\n",
    "接下来需要对特征进行处理。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.preprocessing import LabelEncoder, StandardScaler, OneHotEncoder, OrdinalEncoder\n",
    "import numpy as np\n",
    "\n",
    "def preprocess_data(df):\n",
    "    \"\"\"\n",
    "    1. 对给定的 DataFrame 进行预处理，提取出 features 和 labels.\n",
    "    2. 将数值型的 features 进行标准化，将类别型的 features 转换为相应编码\n",
    "\n",
    "    Args:\n",
    "        df (pandas.DataFrame): The input DataFrame containing the data.\n",
    "\n",
    "    Returns:\n",
    "        - features (numpy.Array): The preprocessed features.\n",
    "        - labels (numpy.Array): The preprocessed labels.\n",
    "    \"\"\"\n",
    "    # TODO\n",
    "\n",
    "    return features, labels\n",
    "\n",
    "\n",
    "features, labels = preprocess_data(df)\n",
    "features.shape, labels.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据集的划分与加载\n",
    "\n",
    "我们按照80%，20%的比例将原数据集分为训练集和测试集。\n",
    "  \n",
    "为了保证实验结果的可复现性，我们设置随机数种子 seed=42。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "X_train, X_test, y_train, y_test = train_test_split(features, labels, test_size=0.2, random_state=42)\n",
    "print(f\"Train dataset size: {len(X_train)}\")\n",
    "print(f\"Test dataset size: {len(X_test)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们创建一个`AbaloneDataset`类，继承自`torch.utils.data.Dataset`，这样方面后续处理与加载。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "\n",
    "\n",
    "class AbaloneDataset(Dataset):\n",
    "    \"\"\"\n",
    "    A custom dataset class for handling abalone data.\n",
    "\n",
    "    Args:\n",
    "        features (numpy.Array): The input features.\n",
    "        labels (numpy.Array): The corresponding labels.\n",
    "\n",
    "    Attributes:\n",
    "        X (torch.Tensor): The input features as a tensor.\n",
    "        y (torch.Tensor): The corresponding labels as a tensor.\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, features, labels):\n",
    "        self.X = torch.tensor(features, dtype=torch.float)\n",
    "        self.y = torch.tensor(labels, dtype=torch.float)\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.X)\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        return self.X[idx], self.y[idx]\n",
    "\n",
    "\n",
    "batch_size =64\n",
    "train_dataset = AbaloneDataset(X_train, y_train)\n",
    "test_dataset = AbaloneDataset(X_test, y_test)\n",
    "train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)\n",
    "test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 构建 MLP 网络\n",
    "\n",
    "我们创建一个多层感知机（MLP）网络，包含一个输入层、任意个隐藏层和一个输出层。其中，输入层的维度为 8，即数据集中的特征数；输出层的维度为 1，即数据集中的标签数。隐藏层的维度可以自行设置。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch.nn as nn\n",
    "\n",
    "class MLP(nn.Module):\n",
    "    def __init__(self, input_size, hidden_size, output_size):\n",
    "        super(MLP, self).__init__()\n",
    "        # TODO\n",
    "    \n",
    "    def forward(self, x):\n",
    "        # TODO\n",
    "        \n",
    "        return x\n",
    "    \n",
    "input_size = X_train.shape[1]\n",
    "hidden_size = 128\n",
    "output_size = 1\n",
    "model = MLP(input_size, hidden_size, output_size)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练与测试\n",
    "\n",
    "我们将训练一个 epoch 的代码封装在`train_epoch`函数中，其中流程如下：\n",
    "\n",
    "1. 遍历训练集，将数据输入模型，得到预测值；\n",
    "2. 计算预测值与真实值之间的差值，然后计算损失；\n",
    "3. 通过反向传播更新模型参数。\n",
    "4. 返回训练集上的平均损失。\n",
    "\n",
    "然后我们实现了一个`eval_model`函数来评估模型的性能：\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Accumulator:\n",
    "    def __init__(self, n):\n",
    "        self.data = [0.0] * n\n",
    "\n",
    "    def add(self, *args):\n",
    "        self.data = [a + float(b) for a, b in zip(self.data, args)]\n",
    "\n",
    "    def reset(self):\n",
    "        self.data = [0.0] * len(self.data)\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        return self.data[idx]\n",
    "\n",
    "\n",
    "def train_epoch(net, device, train_iter, loss_fn, optimizer):\n",
    "    net.train()\n",
    "    metrics = Accumulator(2)\n",
    "    for X, y in train_iter:\n",
    "        X, y = X.to(device), y.to(device)\n",
    "        # TODO\n",
    "\n",
    "        metrics.add(loss * len(y),  len(y))\n",
    "    train_loss = metrics[0] / metrics[1]\n",
    "    return train_loss\n",
    "\n",
    "\n",
    "@torch.no_grad()\n",
    "def eval_model(net, device, test_iter, loss_fn):\n",
    "    net.eval()\n",
    "    metrics = Accumulator(2)\n",
    "    for X, y in test_iter:\n",
    "        X, y = X.to(device), y.to(device)\n",
    "        # TODO\n",
    "        \n",
    "        metrics.add(loss * len(y), len(y))\n",
    "    test_loss = metrics[0] / metrics[1]\n",
    "    return test_loss\n",
    "\n",
    "\n",
    "class RMSLELoss(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(RMSLELoss, self).__init__()\n",
    "\n",
    "    def forward(self, y_hat, y_true):\n",
    "        \"\"\"\n",
    "        定义均方对数误差\n",
    "\n",
    "        Args:\n",
    "            - y_hat(torch.Tensor): the predicted values\n",
    "            - y_true(torch.Tensor): the true values\n",
    "\n",
    "        Returns:\n",
    "            - loss(torch.Tensor): Root Mean Squared Logarithmic Error, RMSLE\n",
    "        \"\"\"\n",
    "        # TODO\n",
    "        \n",
    "        return loss"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "epochs = 50\n",
    "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
    "model = model.to(device)\n",
    "loss_fn = nn.MSELoss()\n",
    "\n",
    "optimizer = torch.optim.Adam(model.parameters(), lr=0.001)\n",
    "train_ls, test_ls = [], []\n",
    "\n",
    "for epoch in range(1, epochs + 1):\n",
    "    train_loss= train_epoch(model, device, train_loader, loss_fn, optimizer)\n",
    "    test_loss = eval_model(model, device, test_loader, loss_fn)\n",
    "    print(\n",
    "        f\"Epoch {epoch}/{epochs} - Train Loss: {train_loss:.6f} - Test Loss: {test_loss:.6f} \"\n",
    "    )\n",
    "    train_ls.append(train_loss)\n",
    "    test_ls.append(test_loss)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "画出模型在训练集和测试集上的 Loss 曲线。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "%config InlineBackend.figure_format='retina'\n",
    "plt.figure(figsize=(6, 4))\n",
    "plt.plot(train_ls, label=\"train loss\")\n",
    "plt.plot(test_ls, label=\"test loss\")\n",
    "plt.xlabel(\"Epoch\")\n",
    "plt.ylabel(\"Loss\")\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 模型评估\n",
    "\n",
    "评估模型在测试集上的 RMSLE"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_iter = DataLoader(test_dataset, batch_size=400, shuffle=False)\n",
    "rmsle = eval_model(model, device, test_iter, RMSLELoss())\n",
    "print(f\"RMSLE: {rmsle:.6f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
